Repository navigation
feat(playground): make baml playground usable over SSH tunnels - #3949
Conversation
…ess guard The guard's port-derived origin allowlist blocked ssh -L remapped tunnels: through a tunnel the browser's origin is the local endpoint — loopback host, arbitrary port. Check host-is-loopback (localhost, 127.0.0.0/8, [::1], http or https, any port) instead of exact strings. The host, not the port, is the DNS-rebinding trust signal; a hostile page's fetch/WS still carries its real remote origin and stays denied. VS Code webview origins are unchanged.
…p banner - --port binds exactly that port via a new bind_exact_port (fails loudly with an actionable message); default stays the auto-scan. - --no-open suppresses the browser; headless sessions (SSH_CONNECTION/ SSH_TTY anywhere, no DISPLAY/WAYLAND_DISPLAY on non-macOS unix) are auto-detected in the CLI and suppress it too. The server crate just obeys a bool on the new PlaygroundServerOptions. - A plain-text stdout banner (URL, project root, ssh -L hint, Ctrl-C note) prints the moment the listener is bound, before project load and independent of RUST_LOG. Browser mode only; stdout is unused there. - webbrowser::open moves onto a detached thread so a text-mode browser (lynx/w3m) can never block server startup. - In browser mode a port-bind failure now propagates as an error instead of limping on to the generic bail; LSP mode keeps serving stdio.
4265 ("BML") is memorable, unregistered, and below the OS ephemeral
ranges; auto-scan covers 4265..4364. Moving off 3700 also separates the
new server's scan range from the legacy engine/language_server (which
deliberately stays on 3700), removing a wrong-instance collision mode.
Doc-comment and test-literal references follow. BAML_PLAYGROUND_DEV_PORT
examples (4000) are the Vite upstream target, not a listen port, and are
unchanged, as is everything under fern/ and engine/.
Data-only YAML topic following the `check` precedent (topic name == subcommand name); covers --from/--file/--port/--no-open, the 4265..4364 scan, headless auto-suppression, and the ssh -L recipe. Snapshot via cargo insta accept, plus the keyword-dispatch regression assertion.
`baml playground` mints a uuid-v4 session token, prints it on the banner URL (?token=...), and the guard middleware rejects /api requests without it (401 with an actionable message). The check runs after the OPTIONS branch so CORS preflights stay unauthenticated; the origin check is unchanged and still wins. This closes the Origin-less exfiltration hole: any local process could previously open /api/ws and receive the env dump. LSP/editor mode mints no token — guard behavior there is byte-for-byte today's, and the VS Code extension is untouched. Frontend: a `withPlaygroundToken` helper in pkg-editor carries the page URL's token onto both WS URLs and the source-files fetch (browsers can't set WS headers, so it rides the query string). The VS Code-injected WS URL is left alone. `WebSocketRuntimePort` now exposes a connection status, and the standalone app shows a thin banner when the socket is stuck retrying — the new user-error path (opening the bare URL without the token) previously rendered a fully dead UI with no explanation. The app's vite config aliases @b/pkg-editor to its source dir, which bypasses the package `exports` map — the new `/token` subpath therefore gets an explicit alias entry so the helper can be imported without pulling the Monaco re-exports into the main bundle.
The first WS frame used to carry the entire process environment. Now the push computes the project-referenced names (salsa-cached `all_env_var_names`) and sends only those that are set. Wire format is unchanged — `processEnvVars` with fewer entries — so no frontend changes; the env panel simply shows only relevant vars. Dynamically-computed keys still resolve lazily through the EnvVarRequest round-trip, which reads the server process env per key and is untouched.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
⏭️ Performance benchmarks were skippedPerf benchmarks (CodSpeed) are opt-in on pull requests — they no longer run on every push. They always run automatically after merge to To run them on this PR, do any of the following, then push a commit (or re-run CI):
|
📝 WalkthroughWalkthroughThis PR adds session-token authorization to the baml playground server, plus configurable port selection ( ChangesPlayground session-token auth and port/browser controls
Estimated code review effort: 4 (Complex) | ~60 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Binary size checks passed✅ 7 passed
Generated by |
## Summary - Remove the per-session playground token minting and API authorization checks. - Restore bare HTTP, WebSocket, and source-file URLs with no `?token=` propagation. - Delete the token helper, package export, Vite alias, UUID dependency, and token-specific connection-status banner. - Remove token documentation and update the playground describe snapshot. - Preserve the SSH tunnel support from BoundaryML#3949, including loopback-origin handling, the 4265 default port, explicit port selection, headless behavior, and filtered environment-variable delivery. ## Why BoundaryML#3949 introduced the session token alongside the SSH playground changes. We have decided not to use token-based authorization for the local playground, so this removes the feature and all supporting structures rather than leaving compatibility plumbing or dead code behind. ## Impact `baml playground` now prints and opens a bare local URL. Playground API and WebSocket requests no longer require a query parameter. The origin guard remains in place: remapped loopback origins used by SSH tunnels are accepted, while hostile remote origins are rejected. ## Validation - `cargo fmt --all -- --check` - `cargo test -p baml_lsp_server --lib` — 24 passed - `cargo test -p baml_cli describe_command_tests` — 89 passed - `pnpm --filter @b/pkg-playground typecheck` - `pnpm --filter @b/pkg-proto generate && pnpm --filter app-vscode-webview build` - Live smoke test with an explicit port: bare URL printed, tokenless remapped-loopback request returned 200, and a hostile origin returned 403 <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Simplified playground access by removing session-token requirements across browser, editor, API, and WebSocket connections. * Playground links now open using plain base URLs, and WebSocket endpoints are derived from the current page (no token query parameters). * **Bug Fixes** * Removed token-dependent banner/retry behavior and adjusted banner output to match the token-free flow. * **Refactor** * Removed public WebSocket connection status reporting and related UI hooks. * **Documentation** * Updated playground keyword documentation to reflect the shortened, token-free URL behavior. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
Problem
baml playgroundwas unusable on a remote machine overssh -L, and risky even locally:/apiguard allowlistedlocalhost:<exact server port>only, so a tunnel likessh -L 8000:localhost:4265loaded the UI but every WS/fetch got 403.webbrowserfalls back to text browsers (lynx/w3m) and the open call sat on the startup path — banner printed, server never served./api/wswith no credentials and receive the entire process environment in the first frame.Changes
Reachability + UX
localhost,127.0.0.0/8,[::1], http/https, any port). The host — not the port — is the DNS-rebinding trust signal; hostile remote origins are still denied. VS Code webview origins unchanged.--port <PORT>(exact bind, loud actionable failure) and--no-open; headless sessions (SSH, or no display on Linux) auto-suppress the browser open.ssh -Lrecipe. Independent ofRUST_LOG.webbrowser::openmoved to a detached thread — a text-mode browser can never block startup.4265..4364.Security (browser mode only)
?token=on the printed/opened URL. The existing/apiguard middleware rejects requests without it (401, actionable message). CORS preflights stay unauthenticated by design; the origin check still runs first. This closes the Origin-less/api/wshole.all_env_var_names, values from the server env). Wire format unchanged. Lazy per-key resolution for dynamic keys is untouched.Documentation:
baml describe playgroundDocs here are agent-native — the repo convention is that the CLI is the documentation (
fern/covers the deprecated legacyengine/stack and is deliberately untouched). The feature gets three self-documenting surfaces, each pinned by tests:baml playground --help— clap doc comments on the new flags, asserted by the CLI help tests.baml describe playground— a new data-only YAML topic inbaml_builtins2/keyword_docs/baml_keywords.yaml(embedded at compile time; zero Rust changes to add). It documents the port scan, headless behavior, thessh -Lrecipe, and the token semantics. Following thecheckprecedent, topic name == subcommand name, so it's guessable frombaml --helpdespite the flat exact-name topic keyspace. Pinned by an insta snapshot of the rendered output plus a dispatch test assertingplaygroundresolves to a keyword topic rather than falling through to "No symbol found".Ports that look related but are deliberately NOT changed
engine/language_serverstays on 3700 — legacy stack; keeping it there separates the old and new servers' scan ranges (removes a wrong-instance collision mode).BAML_PLAYGROUND_DEV_PORT=4000examples — that's the Vite dev server the playground proxies to, an upstream target, not a listen port.fern/port mentions — legacy docs tree, out of scope.Verification
bind_exact_portconflict, help text, describe snapshot.ssh -Lwith a remapped local port): UI loads, both WS endpoints connect, function runs end-to-end; banner/headless/lynx behavior confirmed (server serves while a forced text browser is open).Out of scope (follow-ups noted in the design docs)
--host/non-loopback bind; token for LSP/editor mode +getWebviewHtmlinjection; run-event broadcast hardening (lossy 64-slot channel / full re-broadcast per profile event) — gated on tunnel measurements; multi-tab correctness; anything underfern/orengine/.Summary by CodeRabbit
New Features
Documentation
Bug Fixes